Skip to content

Select text inside tables (#33) - #39

Merged
b451c merged 13 commits into
mainfrom
integrate/table-select
Oct 22, 2026
Merged

b451c merged 13 commits into
mainfrom
integrate/table-select

Conversation

@b451c

@b451c b451c commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

CI gate for the local --no-ff merge of feature/table-select; main will be fast-forwarded to this branch once CI is green.

Tables were atomic in the document selection (selected whole, copied as TSV). Now a word, a whole cell (triple-click) or any range of cells can be selected (asked for on #33).

  • One NSTextView per table with a TextKit 1 NSTextTable string (TableTextBuilder, one paragraph per cell, reading order); the same string is measured off-main, so table rows are .exact. Look unchanged from 1.12 (verified side by side); columns span the full width (FullWidthTextTable).
  • Selection: tables are no longer atomic; hit-testing is cell-confined (points in padding or outside the table snap to the nearest cell); double-click = word, triple-click = cell; copy = covered text inside one cell, TSV across cells, 1.12 TSV for a whole table; Source Edit hint lands on the row's own line.
  • Search counts what the table shows (no markup / link URLs), highlights through the same AppKit path as text rows; no drift after length-changing lowercase (İ).
  • Perf: selection background fills ask only for their own part of the selection (a per-cell fill made ⌘A on a 200-row table quadratic: 7 s / 3 GB → 0.34 s / 69 MB, same as 1.12).

Local: 751 tests, Release + Release-APPSTORE; reviews after each of the three tasks plus a holistic one; E2E on sandboxed and unsandboxed .proto builds (drag in a cell, across cells, header to row, paragraph to table, Shift-click, double/triple-click, ⌘A ⌘C, autoscroll through 200 rows, resize keeping the selection, edge slips, links in cells, Source Edit hint with repeated values, search stepping, zoom, light/dark).

b451c and others added 13 commits October 22, 2026 13:30
…eight

TableTextBuilder builds one attributed string per table (one paragraph per
cell, reading order, NSTextTable blocks); TableTextView (a SelfSizingTextView)
draws it; BlockHeightMeasurer measures the same string, so table rows are
.exact. TableBlockView removed (print keeps PrintableTableView). Search keeps
1.12 behaviour through a bridge until TS-T3. Not shippable alone: tables are
still atomic in the selection until TS-T2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhzVb6aPQs2FF1bHdkcYys
…text

Table matches are counted with the same matcher over the same characters
the table view shows (the measurer's TableText string, or a plain string
from the same builder helpers), so count == highlights, in reading order;
markup and link URLs are no longer counted. Highlight through
applyNSSearchHighlight (moved to DocumentSearch.swift for headless tests).
The 1.12 bridge and the dead SwiftUI highlight path are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhzVb6aPQs2FF1bHdkcYys
… no rebuild per pass

- FullWidthTextTable: whole-point columns that sum to the full width (the
  remainder 1 pt each to the first columns), so the header band and rules
  reach the outline (NSTextTable floored every column: up to n-1 pt short).
- TableText.Source carries theme name, dark/light and document fonts;
  matches(theme:fontScale:) rejects an instance built for another look.
- The representable builds or adopts a TableText only when its source
  changes; the background colour is compared by value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhzVb6aPQs2FF1bHdkcYys
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhzVb6aPQs2FF1bHdkcYys
Tables are no longer atomic in the document selection: the controller asks
the row's text view for the character under the pointer (cell-confined in
tables), multi-click selects a word or the cell, and copy builds the covered
text of one cell, or TSV of the covered cells (partial first/last cell), or
the 1.12 TSV for a whole table. Per-row selection rules moved to a pure
SelectionRows.swift so they are unit-tested; Source Edit hint lands on the
table's line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhzVb6aPQs2FF1bHdkcYys
searchMatchRanges returned ranges in the lowercased copy's coordinates;
"İ" lowercases to two UTF-16 units, so every later highlight of a row (a
whole table) shifted by one. When the lowercased length differs, search
the original string with .caseInsensitive; otherwise unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhzVb6aPQs2FF1bHdkcYys
- multi-click on text rows is exactly the pre-v1.13 glyph rule; the cell
  rule compares against the glyph's composed character range (emoji, ZWJ)
- Source Edit hint inside one table band searches that band's source line
  only (repeated values in a column no longer land on another row)
- a single-cell copy drops the column's alignment as well as the text block

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhzVb6aPQs2FF1bHdkcYys
…selection

A table fills a background and borders per cell, and every fill computed
the rects of the WHOLE selection to cut its holes: quadratic. ⌘A ⌘C on a
200-row table took 7 s on first draw and grew the process to 3 GB (1.12:
0.35 s, 110 MB). `selectionRects(in:limitedTo:)` takes the fill's
character range and keeps the whole covered range as the selected range,
so line ends extend exactly as before; the limited rects add up to the
whole selection's (test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhzVb6aPQs2FF1bHdkcYys
- hit-test: a table clamps the point into its view and asks the cell hook
  in every direction (TS-D6); text rows keep their edges
- whole-table copy reads the built cells (one source of truth, no second
  renderInline pass), 1.12 parity pinned on rich fixtures
- text rows are counted with searchMatchRanges, so count == highlights
- drop the unused 4-arg TableText.matches and TableTextLayout.bandRange;
  stale comments (tables are .exact, on-screen cells are AppKit)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhzVb6aPQs2FF1bHdkcYys
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhzVb6aPQs2FF1bHdkcYys
@b451c
b451c merged commit 2eda1d5 into main Oct 22, 2026
3 checks passed
@b451c
b451c deleted the integrate/table-select branch October 22, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant